<?php
/**
 *基础模型类
 *所有的数据表操作模型类需要继承此类
 *@package Models
 *@author 江羿平<jyp@cjkt.com>
 *@todo 分库时查询
 */
namespace Models;

use JsonSerializable;
use CI_Model;
use ArrayAccess;
use BadMethodCallException;
use InvalidArgumentException;
use ReflectionClass;
use RunTimeException;
use CI_DB_query_builder;
use Libraries\Paginator;
use Models\Exception\ModelNotFoundException;

Class Model extends CI_Model implements ArrayAccess, JsonSerializable{

    /**
     * 表名
     * @var integer
     */
    protected $_table = '';

    /**
     * 主键
     * @var integer
     */
    protected $_primekey = 'id';

    /**
     * 默认筛选条件
     * @var string
     */
    protected $_default_filter = 'isdel = 0';

    /**
     * 默认筛选条件开关
     * @var bool
     */
    protected $_filter = TRUE;

    /**
     * @var array 关联预加载
     */
    protected $with = array();

    /**
     * @var array 预加载查询字段
     */
    protected $with_select = array();

    /**
     * @var array 预加载数据
     * eg: array(
            'data' => array(xxx),
            'relation' => 'has_one',
        )
     */
    protected $eagerLoads = array();

    /**
     * 一对一关联配置
     *
     * 配置格式为：标识符=>['table'=>表名,'foreign'=>外键, 'class' => model].
     *
     * eg:array('student'=>['table'=>'cjkt_studnet','foreign'=>'uid'])
     *
     * @var array
     */
    protected $_has_one = array();

    /**
     * 一对多关联配置
     *
     * 配置格式为：标识符=>['table'=>表名,'foreign'=>外键, 'class' => model].
     *
     * eg:array('order'=>['table'=>'cjkt_order','foreign'=>'uid'])
     *
     * @var array
     */
    protected $_has_many = array();

    /**
     * 多对一关联配置
     *
     * 配置格式为：标识符=>['table'=>表名,'foreign'=>外键,'belongs_key'=>本表的关联键, 'class' => model].
     *
     * eg:array('user'=>['table'=>'cjkt_user','foreign'=>'id','belongs_key'=>'uid'])
     */
    protected $_belongs = array();

    /**
     * 字段数据
     * @var array
     */
    protected $_fields = array();

    /**
     * @var array 允许批量赋值的数据
     */
    protected $allows = array();
    /**
     * 软删除字段
     *
     */
    protected $_softDeleteField = 'isdel';

    /**
     * @var bool 是否是新纪录
     */
    protected $is_new = true;

    /**
     * 加载数据
     * @param array $data 数据
     * @return bool
     */
    public function load($data){
        if(!$this->allows) {
            $fields = $this->db->list_fields($this->_table);
            $this->allows = $fields;
        }
        foreach ($data as $k => $v) {
            if($this->allows && !in_array($k , $this->allows))
                continue;
            $this->_fields[$k] = $v;
        }

        return $this;
    }

    /**
     * 获取表名
     * @return string
     */
    public function get_table(){
        return $this->_table;
    }

    /**
     *通过主键查询
     *@param integer   $id       主键ID
     *@return Array
     */
    public function find($id){
        if(is_array($id)){
            $this->db->where_in($this->_primekey,$id);
            return $this->get();
        }else{
          $this->db->where($this->_primekey,$id);
          $this->db->limit(1);
          if($this->_filter && $this->_default_filter){
            $this->db->where($this->_default_filter);
          }
          $this->_fields = $this->db->get($this->_table)->row_array();
          if(!$this->_fields) return false;
          $this->is_new = false;
          return $this;
        }
    }

    /**
     * 通过之间查询，失败抛出异常
     * @param int $id 主键
     */
    public function findOrFail($id)
    {
        $this->_fields = $this->db->where($this->_primekey, $id)->limit(1)->get($this->_table)->row_array();
        if (!$this->_fields) {
            throw new ModelNotFoundException();
        }
        $this->is_new = false;

        return $this;
    }

    /**
     *插入数据
     *@param  array $fields 要插入数据的数组
     *@return integer | 插入后的ID
     */
    public function add($fields = NULL) {
        if($fields)
        $this->db->insert($this->_table, $fields);
        else {
            if(isset($this->_fields[$this->_primekey])) unset($this->_fields[$this->_primekey]);
            $this->db->insert($this->_table,$this->_fields);
        }
        $insert_id =  $this->db->insert_id();
        if($insert_id){
            $this->_fields[$this->_primekey] = $insert_id;
            $this->is_new = false;
            return $insert_id;
        }else{
            return FALSE;
        }
    }

    /**
     * 持久化保存模型
     *
     * @return Models\Model|false
     */
    public function save()
    {
        if ($this->is_new) {
            return $this->add();
        } else {
            return $this->update();
        }
    }

    /**
     *更新数据
     *@param integer   $id       主键ID
     *@param array     $fields   要更新的数据数组
     *@return integer 更新条数
     */
    public function update($id = NULL, $fields = NULL) {
        if($id === NULL && isset($this->_fields[$this->_primekey])) $id = $this->_fields[$this->_primekey];
        if($id) {
            if(is_array($id)) $this->db->where_in($this->_primekey, $id);
            else $this->db->where($this->_primekey, $id);
            if(!$fields) $fields = $this->_fields;
            if(!count($fields)) return 0;
            if(isset($fields[$this->_primekey])) unset($fields[$this->_primekey]);
            $this->db->update($this->_table, $fields);
            return $this->db->affected_rows();
        } else {
            throw new InvalidArgumentException("bad argument id,you may want to use updateAll()", -1);
        }

    }

    /**
     * 批量修改数据
     * @param  array $arr    批量数据
     * @param  string $where where条件
     * @return number        修改记录数
     */
    public function multi_update($arr,$where=''){
        $sql = "update ".$this->_table.' SET ';
        if(isset($arr['field'])){
            $arr = [$arr];
        }

        $i = 1;
        foreach ($arr as $itm) {
            $sql .= $this->multi_update_item($itm['field'],$itm['key'],$itm['data']);
            if($i<count($arr)) $sql .=',';
            $i++;
        }

        if($where){
            $sql .=" where ".$where;
        }

        return $this->execute($sql);
    }

    /**
     * 生成批量修改语句
     * @param  string $field 要修改的字段
     * @param  string $key   判断条件字段
     * @param  array  $arr   条件数组，['1'=>'3'];
     * @return string        SQL语句
     */
    private function multi_update_item($field,$key,$arr){
        $sql = ' `'.$field.'` = CASE `'.$key.'`';
        foreach ($arr as $k => $v) {
            $sql .=" WHEN '".$k."' THEN '".$v."'";
        }
        $sql .=" END ";
        return $sql;
    }

    /**
     * 更新所有数据
     * @param array     $fields   要更新的数据数组
     * @return integer 更新条数
     */
    public function updateAll($fields = array()){
        $this->db->update($this->_table, $fields);
        return $this->db->affected_rows();
    }

    /**
     * 删除数据
     * @param  integer|array $id 主键ID或ID数组
     * @return integer 删除的条数
     */
    public function delete($id = NULL){
        if(is_array($id)){
            $this->db->where_in($this->_primekey, $id)->delete($this->_table);
            return $this->db->affected_rows();
        }else if(intval($id)){
            $this->db->where($this->_primekey, $id)->delete($this->_table);
            return $this->db->affected_rows();
        }else if(isset($this->_fields[$this->_primekey]) && intval($this->_fields[$this->_primekey])){

            $this->db->where($this->_primekey, $this->_fields[$this->_primekey])->delete($this->_table);
            return $this->db->affected_rows();
        }else{
            throw new InvalidArgumentException('need id, you may want to use method deleteAll', 1);
        }
    }

    public function deleteAll()
    {
        $this->db->delete($this->_table);
    }

    /**
    * 增加计数
    * @param    int     $id             ID
    * @param    string  $field          字段
    * @param    int     $increment_num  步长
    * @return   void
    */
    public function increment($field,$id,$increment_num = 1){
        $field = '`'.$field.'`';
        $this->db->set($field, $field.'+'.$increment_num, FALSE);
        if(!$id) $id = $this->_fields[$this->_primekey];
        if($id) {
           if(is_array($id)) $this->db->where_in($this->_primekey, $id);
           else $this->db->where($this->_primekey, $id);
        }
        $this->db->update($this->_table);
    }

    /**
    * 减少计数
    * @param    int     $id             ID
    * @param    string  $field          字段
    * @param    int     $increment_num  步长
    * @return
    */
    public function decrement($field,$id,$increment_num = 1){
        $field = '`'.$field.'`';
        $this->db->set($field, $field.'-'.$increment_num, FALSE);
        if(!$id) $id = $this->_fields[$this->_primekey];
        if($id) {
           if(is_array($id)) $this->db->where_in($this->_primekey, $id);
           else $this->db->where($this->_primekey, $id);
        }
        $this->db->update($this->_table);
    }

    /**
     *计数
     * @param Closure  $build  查询条件构造函数
     * @param bool 是否重置查询
     * @return integer
     */
    public function counts($build = NULL, $reset = true){
        if($build){
            $build($this->db);
        }
        if($this->_filter && $this->_default_filter) $this->db->where($this->_default_filter);
        $this->db->from($this->_table);
        return $this->db->count_all_results(null, (bool) $reset);
    }

    /**
     *计数
     * @param Closure  $build  查询条件构造函数
     * @param bool 是否重置查询
     * @return integer
     */
    public function count($build = NULL, $reset = true) {
        return $this->counts($build, $reset);
    }

    /**
     * 一对一关联查询
     * @param integer     $id        当前主键ID
     * @param array      $relation     关系数组
     * - class: 关联类
     * - foreign 关联键
     * @return Model
     */
    protected function has_one($id = NULL,$table,$relation){
        if(!$id) $id = $this->_fields[$this->_primekey];
        $model = new $relation['class'];

        return $model->where($model->get_table().".{$relation['foreign']}", $id)
        ->get_one() ?: null;
    }

    /**
     * 一对多关联查询
     * @param integer     $id        当前主键ID
     * @param array       $relation  关系数组
     * - class: 关联类
     * - foreign 关联键
     *@return Array
     */
    protected function has_many($id = NULL, $relation){
        if(!$id) $id = $this->_fields[$this->_primekey];
        $model = new $relation['class'];

        return $model->where($model->get_table().".{$relation['foreign']}", $id)
            ->get();
    }

    /**
     *多对一关联查询
     * @param integer     $id        当前主键ID
     * @param array       $relation  关系数组
     * - class: 关联类
     * - foreign: 关联键
     * - belongs_key: 本表属于键
     * @return Model
     */
    protected function belongs($id = NULL,$relation){
        if(!$id) $id = $this->_fields[$relation['belongs_key']];
        $model = new $relation['class'];

        return $model->where($model->get_table().".{$relation['foreign']}", $id)->get_one() ?: null;
    }

    /**
     * 设置预加载关系数据
     * 设置预加载数据，当调用get时,为所有关系数据赋值
     * @param string | array 预加载关系
     * @return Models\Model
     */
    public function with($relations,$select = null)
    {
        if(is_string($relations)) {
            $arr = array_filter(explode(',', $relations));
            $relations = [];
            foreach ($arr as  $name) {
                $relations[$name] = null;
            }
        }
        $relationships = array_merge($this->_has_one, $this->_has_many, $this->_belongs);

        foreach ($relations as $name => $callable) {
            if (is_int($name)) {
                $name = $callable;
            }

            if ($select) {
                $callable = function () use ($select) {
                    $this->select($select);
                };
            }

            if(false === array_key_exists($name, $this->with)) {
                if (!isset($relationships[$name])) {
                    throw new InvalidArgumentException('model has not the relationship :'.$name, -1);
                }

                $this->with[$name] = $callable;
            }
        }

        return $this;
    }

    /**
     *分页
     *@param integer $page_size  每页显示条数
     *@param integer $page_num   当前页数
     *@return array
     */
    public function paginate($page_size,$page_num = NULL){

        if(!$page_num){
            $page_num = $this->input->get('page');
            if(!$page_num) $page_num = 1;
        }
        if(!$page_size) $page_size = 10;

        if($this->_filter && $this->_default_filter) $this->db->where($this->_default_filter);
        $total = $this->db->count_all_results($this->_table,FALSE);

        $this->db->limit($page_size,($page_num-1)*$page_size);
        $query = $this->db->get();
        $data = $query->result_array();
        if (method_exists($this, 'filter')) {
            foreach ($data as $key => $value) {
                $data[$key] = $this->filter($value);
            }
        }
        $data = $this->withRelationShips($data);
        $config = array('data'=>$data,'total'=>$total,'page_size'=>$page_size,'current_page'=>$page_num);

        $paginator = new Paginator();
        $paginator->make($total,$page_size,$page_num,$data);
        return $paginator;
    }

    /**
     * 条件查询
     * @param  bool   $default_filter  是否使用默认筛选条件
     * @return array
     */
    public function get(){
        if($this->_filter && $this->_default_filter) $this->db->where($this->_default_filter);
        $result = $this->db->get($this->_table)->result_array();

        // 关系数据
        if ($this->with) {
            $result = $this->withRelationShips($result);
        }
        // 过滤器
        if (method_exists($this, 'filter')) {
            foreach ($result as $key => $value) {
                $result[$key] = $this->filter($value);
            }
        }

        return $result;
    }

    /**
     * 执行语句获取资源
     * @param bool $default_filter  是否使用默认筛选条件
     * @return CI_DB_result
     */
    public function get_resource(){
        if($this->_filter && $this->_default_filter) $this->db->where($this->_default_filter);
        return $this->db->get($this->_table);
    }

    /**
     * 获取一条记录
     * @param string $type 返回类型
     * @return array
     */
    public function get_one($type = NULL){
        if($this->_filter && $this->_default_filter) $this->db->where($this->_default_filter);
        $result = $this->db->limit(1)->get($this->_table)->row_array();
        if(!$result) return FALSE;
        $this->_fields = $result;
        if($type == 'array'){
            return $result;
        }
        $this->is_new = false;

        return $this;
    }


    /**
     * 获取一条记录或抛出异常
     *
     * @param string 返回类型
     * @return Model|array
     */
    public function get_one_or_fail($type = null)
    {
        $class = get_called_class();
        $result = $this->get_one($type);
        if (!$result) {
            throw new ModelNotFoundException('未找到'.$class.'相关记录', 404);
        }
        return $this;
    }

    /**
     * SQL语句查询
     * @param string   $sql           SQL语句
     * @param bool     $result_type   返回形式|TRUE=>row_array FALSE=>result_array
     * @return array
     */
    public function query($sql,$result_type = FALSE){
        $query = $this->db->query($sql);
        if($result_type) return $query->row_array();
        else return $query->result_array();
    }

    /**
     * SQL执行语句
     * @param string   $sql           SQL语句
     */
    public function execute($sql){
        $this->db->query($sql);
    }

    public function soft_delete($id = null){
        if(null === $id && empty($this->_fields[$this->_primekey])){
            throw new InvalidArgumentException("need id", 1);
        }
        if(null === $id)
            $id = $this->_fields[$this->_primekey];
        $update = array();
        $update[$this->_softDeleteField] = 1;

        return $this->db->where($this->_primekey, $id)->update($this->_table , $update);
    }

    public function soft_deleteAll()
    {
        return $this->db->update($this->_table , array($this->_softDeleteField => 1));
    }

    /**
     * 将结果集携带关系字段
     *
     * @param array 结果集
     * @return array
     */
    protected function withRelationShips ($result) {
        $eager_data = $this->getEagerData($result);

        $result = array_map(function ($value) use ($eager_data) {
            foreach ($eager_data as $name => $eager) {
                if ($eager['relation'] == 'has_one') {
                    $value[$name] = $eager['data'][$value[$this->_primekey]] ? $eager['data'][$value[$this->_primekey]] : null;
                } else if ($eager['relation'] == 'has_many') {
                    $value[$name] = array();
                    foreach ($eager['data'] as $eager_row) {
                        if($eager_row[$eager['foreign']] == $value[$this->_primekey])
                            $value[$name][] = $eager_row;
                    }
                } else if ($eager['relation'] == 'belongs') {
                    $value[$name] = isset($eager['data'][$value[$eager['belongs_key']]]) ? $eager['data'][$value[$eager['belongs_key']]] : null;
                }
            }

            return $value;
        }, $result);

        return $result;
    }

    /**
     * 获取预加载关系数据
     *
     * @param array 结果集
     * @return array 关系数据
     *      eg: array(
     *          array('data' => array(), 'relation' => 'has_one', 'foreign' => '')
     *      )
     */
    protected function getEagerData ($result) {
        if(!$result)
            return $result;
        $ids = array();
        $needGets = array();
        foreach ($this->_belongs as $key => $value) {
            ${$value['belongs_key'].'s'} = array();
            $needGets[$value['belongs_key']] = $value['belongs_key'];
        }
        foreach ($result as $k => $v) {
            $ids[] = $v[$this->_primekey];

            foreach ($needGets as $key => $value) {
                ${$key.'s'}[] = $v[$key];
            }
        }
        $eager_data = array();
        foreach ($this->with as $name => $callable) {
            if (isset($this->_has_one[$name]) or isset($this->_has_many[$name])) {
                $relation = isset($this->_has_one[$name]) ? $this->_has_one[$name]
                                : $this->_has_many[$name];
                $relation_name = isset($this->_has_one[$name]) ? 'has_one' : 'has_many';
                if (isset($relation['class'])) {
                    $model = new $relation['class'];
                    if (is_callable($callable)) {
                        $callable($model);
                    }

                    $data = arr_key($model->where_in($relation['foreign'], $ids)->default_filter($this->_filter)->get());
                } else {
                    $data = arr_key($this->db->select($select_fileds)->where_in($relation['foreign'], $ids)->get($relation['table'])->result_array(), $relation['foreign']);
                }
            } else if(isset($this->_belongs[$name])) {
                $relation = $this->_belongs[$name];
                $relation_name = 'belongs';
                if(isset($relation['class'])){
                    $model = new $relation['class'];
                    if (is_callable($callable)) {
                        $callable($model);
                    }

                    $data = arr_key($model->where_in($relation['foreign'], ${$relation['belongs_key'].'s'})->default_filter($this->_filter)->get());
                } else {
                    $data = arr_key($this->db->where_in($relation['foreign'], ${$relation['belongs_key'].'s'})->get($relation['table'])->result_array(), $relation['foreign']);
                }
            }

            $eager_data[$name] = array_merge(
                $relation,
                array(
                    'data' => $data,
                    'relation' => $relation_name
                )
            );
        }

        return $eager_data;
    }

    /**
     * 重载方法，自动查询关联数据
     *@param  string  $function_name    方法名称
     *@param  array   $args             参数
     *@return array|null|bool
     */
    public function __call($function_name,$args){
        if(isset($this->_has_one[$function_name])){
            //一对一关联
            $id = NULL;
            if(is_array($args) && count($args)>0) $id = $args[0];

            return $this->has_one($id, $this->_has_one[$function_name]);
        }else if(isset($this->_has_many[$function_name])){
            //一对多关联
            $id = NULL;
            if(is_array($args) && count($args)>0) $id = $args[0];

            return $this->has_many($id, $this->_has_many[$function_name]);
        }else if(isset($this->_belongs[$function_name])){
            //多对一关联
            $id = NULL;
            if(is_array($args) && count($args)>0) $id = $args[0];

            return $this->belongs($id,$this->_belongs[$function_name]);
        }else{
            call_user_func_array(array($this->db,$function_name),$args);
            return $this;
        }
    }

    /**
     * 魔术方法
     * 该函数实现ORM以queryBulder方式对数据库的操作
     * @param string method 名称
     * @param array 参数数组
     * @return mixed
     */
    public static function __callStatic($name, $arguments)
    {
        $class = get_called_class();
        $model = new $class();
        $reflect = new ReflectionClass(CI_DB_query_builder::Class);
        if ($reflect->hasMethod($name)) {
            return call_user_func_array(array($model, $name), $arguments);
        }

        throw new BadMethodCallException('Call a undefined method on '.get_called_class());
    }

    public function jsonSerialize()
    {
        return (array) $this->toArray();
    }

    public function toArray()
    {
        return $this->to_array();
    }

    /**
    * 设置默认筛选条件开关
    * @param    bool  $is_open
    * @return   void
    */
    public function default_filter($is_open){
        $this->_filter = $is_open;
        return $this;
    }

    /**
     * 清空数据
     * @return void
     */
    public function clean(){
        $this->_fields = array();
    }

    /**
     * 返回数组
     * @return array
     */
    public function to_array(){
        if (method_exists($this, 'filter')) {
            return $this->filter($this->_fields);
        }
        return $this->_fields;
    }

    /**
     * 转化为json
     * @return string
     */
    public function to_json(){
        return json_encode($this->_fields);
    }

    /*接口方法*/
    public function offsetExists($offset){
        return isset($this->_fields[$offset]);
    }

    public function offsetSet($offset, $value){
        $this->_fields[$offset] = $value;
    }

    public function &offsetGet($offset){
        return $this->_fields[$offset];
    }

    public function offsetUnset($offset){
        unset($this->_fields[$offset]);
    }

}
